/*
 * Copyright (C) 2011 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.testing;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.annotations.GwtCompatible;
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import java.util.List;

import junit.framework.AssertionFailedError;

/**
 * Implementation helper for {@link EqualsTester} and {@link EquivalenceTester} that tests for
 * equivalence classes.
 *
 * @author Gregory Kick
 */
@GwtCompatible
final class RelationshipTester<T>
{

    static class ItemReporter
    {
        String reportItem(Item<?> item)
        {
            return item.toString();
        }
    }

    /**
     * A word about using {@link Equivalence}, which automatically checks for {@code null} and
     * identical inputs: This sounds like it ought to be a problem here, since the goals of this class
     * include testing that {@code equals()} is reflexive and is tolerant of {@code null}. However,
     * there's no problem. The reason: {@link EqualsTester} tests {@code null} and identical inputs
     * directly against {@code equals()} rather than through the {@code Equivalence}.
     */
    private final Equivalence<? super T> equivalence;

    private final String relationshipName;
    private final String hashName;
    private final ItemReporter itemReporter;
    private final List<ImmutableList<T>> groups = Lists.newArrayList();

    RelationshipTester(
            Equivalence<? super T> equivalence,
            String relationshipName,
            String hashName,
            ItemReporter itemReporter)
    {
        this.equivalence = checkNotNull(equivalence);
        this.relationshipName = checkNotNull(relationshipName);
        this.hashName = checkNotNull(hashName);
        this.itemReporter = checkNotNull(itemReporter);
    }

    // TODO(cpovirk): should we reject null items, since the tests already check null automatically?
    public RelationshipTester<T> addRelatedGroup(Iterable<? extends T> group)
    {
        groups.add(ImmutableList.copyOf(group));
        return this;
    }

    public void test()
    {
        for (int groupNumber = 0; groupNumber < groups.size(); groupNumber++)
        {
            ImmutableList<T> group = groups.get(groupNumber);
            for (int itemNumber = 0; itemNumber < group.size(); itemNumber++)
            {
                // check related items in same group
                for (int relatedItemNumber = 0; relatedItemNumber < group.size(); relatedItemNumber++)
                {
                    if (itemNumber != relatedItemNumber)
                    {
                        assertRelated(groupNumber, itemNumber, relatedItemNumber);
                    }
                }
                // check unrelated items in all other groups
                for (int unrelatedGroupNumber = 0;
                     unrelatedGroupNumber < groups.size();
                     unrelatedGroupNumber++)
                {
                    if (groupNumber != unrelatedGroupNumber)
                    {
                        ImmutableList<T> unrelatedGroup = groups.get(unrelatedGroupNumber);
                        for (int unrelatedItemNumber = 0;
                             unrelatedItemNumber < unrelatedGroup.size();
                             unrelatedItemNumber++)
                        {
                            assertUnrelated(groupNumber, itemNumber, unrelatedGroupNumber, unrelatedItemNumber);
                        }
                    }
                }
            }
        }
    }

    private void assertRelated(int groupNumber, int itemNumber, int relatedItemNumber)
    {
        Item<T> itemInfo = getItem(groupNumber, itemNumber);
        Item<T> relatedInfo = getItem(groupNumber, relatedItemNumber);

        T item = itemInfo.value;
        T related = relatedInfo.value;
        assertWithTemplate(
                "$ITEM must be $RELATIONSHIP to $OTHER",
                itemInfo,
                relatedInfo,
                equivalence.equivalent(item, related));

        int itemHash = equivalence.hash(item);
        int relatedHash = equivalence.hash(related);
        assertWithTemplate(
                "the $HASH ("
                        + itemHash
                        + ") of $ITEM must be equal to the $HASH ("
                        + relatedHash
                        + ") of $OTHER",
                itemInfo,
                relatedInfo,
                itemHash == relatedHash);
    }

    private void assertUnrelated(
            int groupNumber, int itemNumber, int unrelatedGroupNumber, int unrelatedItemNumber)
    {
        Item<T> itemInfo = getItem(groupNumber, itemNumber);
        Item<T> unrelatedInfo = getItem(unrelatedGroupNumber, unrelatedItemNumber);

        assertWithTemplate(
                "$ITEM must not be $RELATIONSHIP to $OTHER",
                itemInfo,
                unrelatedInfo,
                !equivalence.equivalent(itemInfo.value, unrelatedInfo.value));
    }

    private void assertWithTemplate(String template, Item<T> item, Item<T> other, boolean condition)
    {
        if (!condition)
        {
            throw new AssertionFailedError(
                    template
                            .replace("$RELATIONSHIP", relationshipName)
                            .replace("$HASH", hashName)
                            .replace("$ITEM", itemReporter.reportItem(item))
                            .replace("$OTHER", itemReporter.reportItem(other)));
        }
    }

    private Item<T> getItem(int groupNumber, int itemNumber)
    {
        return new Item<>(groups.get(groupNumber).get(itemNumber), groupNumber, itemNumber);
    }

    static final class Item<T>
    {
        final T value;
        final int groupNumber;
        final int itemNumber;

        Item(T value, int groupNumber, int itemNumber)
        {
            this.value = value;
            this.groupNumber = groupNumber;
            this.itemNumber = itemNumber;
        }

        @Override
        public String toString()
        {
            return value + " [group " + (groupNumber + 1) + ", item " + (itemNumber + 1) + ']';
        }
    }
}
